#include <stdlib.h>
#include <string.h>

#ifdef UNIX
# include <unistd.h>
# include <dirent.h>
# include <sys/stat.h>
#else
# include <direct.h>
# include <windows.h>
#endif

#include "../log.h"
#include "../memmap.h"
#include "dirfs.h"

void dfs_init(DirFileSystem *dfs)
{
    dfs->nument = 0;
    dfs->basePath = NULL;
}

void dfs_setBasePath(DirFileSystem *dfs, const char *path)
{
    u8 appendSlash = 0;
    appendSlash = (path[strlen(path) - 1] != '/');
    if (dfs->basePath)
        dfs->basePath = realloc(dfs->basePath, strlen(path) + 1 + appendSlash);
    else
        dfs->basePath = malloc(strlen(path) + 1 + appendSlash);

    dfs->basePath[0] = '\0';
    strcpy(dfs->basePath, path);
    if (appendSlash)
        strcat(dfs->basePath, "/");
#ifdef UNIX
    mkdir(path, 0755);
#else
    _mkdir(path);
#endif
}

void dfs_free(System *sys)
{
    DirFileSystem *dfs = sys->system;
    free(dfs->basePath);
    dfs->basePath = NULL;
}

char *dfs_getRealPath(System *sys, const char *localpath)
{
    DirFileSystem *dfs = (DirFileSystem*)sys->system;

    char *newpath;

    /* Convert slashes */
    if (localpath[0] == '/')
        localpath = localpath + 1;

    if (strchr(localpath, ':'))
    {
        u32 i;
        for (i = 0; i < strlen(localpath); i++)
            if (localpath[i] == ':')
                break;
        if (localpath[i + 1] == '\0') /* handle pathes like 'ms0:' */
            localpath = localpath + i + 1;
        else
            localpath = localpath + i + 2;
    }

    newpath = malloc(strlen(dfs->basePath) + strlen(localpath) + 1);
    newpath[0] = '\0';
    strcpy(newpath, dfs->basePath);
    strcat(newpath, localpath);

    return newpath;
}

void dfs_freeRealPath(char *path)
{
    free(path);
}

s32 dfs_openFile(System *sys, const char *filename, FileAccess access)
{
    DirFileSystem *dfs = (DirFileSystem*)sys->system;
    char *fullname;
    FILE *entry;

    fullname = dfs_getRealPath(sys, filename);

    if (access & FILEACCESS_CREATE) {
        entry = fopen(fullname, "w");
        fclose(entry);
        entry = fopen(fullname, "r");
    }
    else
        entry = fopen(fullname, "r");

    dfs_freeRealPath(fullname);

    if (entry != NULL)
    {
        dfs->handles[dfs->nument] = mfs_getNewHandle();
        dfs->entries[dfs->nument] = entry;
        dfs->nument++;
        return mfs_getHandle();
    }
    return 0;
}

s32 dfs_ownsHandle(System *sys, u32 handle)
{
    DirFileSystem *dfs = (DirFileSystem*)sys->system;
    u32 i;
    for (i = 0; i < dfs->nument; i++)
        if (dfs->handles[i] == handle)
            return 1;
    return 0;
}

s32 dfs_getEntry(DirFileSystem *dfs, u32 handle)
{
    u32 i;
    for (i = 0; i < dfs->nument; i++)
        if (dfs->handles[i] == handle)
            return i;
    return -1;
}

size_t dfs_readFile(System *sys, u32 handle, char *pointer, u32 size)
{
    DirFileSystem *dfs = (DirFileSystem*)sys->system;
    s32 entry = dfs_getEntry(dfs, handle);

    if (entry != -1)
        return fread(pointer, 1, size, dfs->entries[entry]);

    _log(WRN, FS, "Hey, what are you doing? Reading non-open files?");
    return 0;
}

size_t dfs_readFileToAddr(System *sys, u32 handle, u32 addr, unsigned size)
{
    DirFileSystem *dfs = (DirFileSystem*)sys->system;
    s32 entry = dfs_getEntry(dfs, handle);

    if (entry != -1)
    {
        s32 c;
        u32 i;
        for (i = 0; i < size && (c = fgetc(dfs->entries[entry])) != EOF; i++)
            mem_write8(addr + i, c);
        return i;
    }

    _log(WRN, FS, "Hey, what are you doing? Reading non-open files?");
    return 0;
}

FileInfo dfs_getFileInfo(System *sys, const char *filename)
{
    FileInfo x;

    char *fullname;
    FILE *file;

    x.size = 0;
    fullname = dfs_getRealPath(sys, filename);

    file = fopen(fullname, "rb");

    dfs_freeRealPath(fullname);

    if (file == NULL)
        return x;

    fseek(file, 0, SEEK_END);
    x.size = ftell(file);
    fseek(file, 0, SEEK_SET);

    fclose(file);

    return x;
}

s32 dfs_seekFile(System *sys, u32 handle, s32 position, FileMove type)
{
    DirFileSystem *dfs = (DirFileSystem*)sys->system;
    s32 entry = dfs_getEntry(dfs, handle);

    if (entry != -1)
    {
        s32 whence;
        switch (type)
        {
        case FILEMOVE_BEGIN:
            whence = SEEK_SET;
            break;

        case FILEMOVE_CURRENT:
            whence = SEEK_CUR;
            break;

        case FILEMOVE_END:
            whence = SEEK_END;
            break;

        default:
            _log(ERR, FS, "Unrecognized seek type !");
            return 0;
        }
        fseek(dfs->entries[entry], position, whence);
        return ftell(dfs->entries[entry]);
    }

    _log(WRN, FS, "Trying to seek non-opened file ?");
    return 0;
}

s32 dfs_writeFile(System *sys, u32 handle, u8 *pointer, u32 size)
{
    DirFileSystem *dfs = (DirFileSystem*)sys->system;
    s32 entry = dfs_getEntry(dfs, handle);

    if (entry != -1)
        return fwrite(pointer, sizeof(char), size, dfs->entries[entry]);

    _log(WRN, FS, "Trying to write to non-opened file ?");
    return 0;
}

s32 dfs_writeFileFromAddr(System *sys, u32 handle, u32 addr, u32 size)
{
    DirFileSystem *dfs = (DirFileSystem*)sys->system;
    s32 entry = dfs_getEntry(dfs, handle);

    if (entry != -1)
    {
        u32 i;
        for (i = 0; i < size && fputc(mem_read8(addr + i), dfs->entries[entry]) != EOF; i++)
            ;
        return i;
    }

    _log(WRN, FS, "Trying to write to non-opened file ?");
    return 0;
}

s8 dfs_mkdir(System *sys, const char *dir, s32 mode)
{
    char *fullname = dfs_getRealPath(sys, dir);
    s32 ret;

    _log(INF, FS, "Creating %s with mode %03o", dir, mode);

#ifdef UNIX
    ret = mkdir(fullname, mode);
#else
    ret = _mkdir(fullname);
#endif
    dfs_freeRealPath(fullname);
    return ret;
}
   
void dfs_closeFile(System *sys, u32 handle)
{
    DirFileSystem *dfs = (DirFileSystem*)sys->system;
    s32 entry = dfs_getEntry(dfs, handle);
    u32 i;

    if (entry != -1)
        fclose(dfs->entries[entry]);
    else
        _log(WRN, FS, "Trying to close non-opened file ?");

    dfs->nument--;
    for (i = entry; i < dfs->nument; i++) {
        dfs->handles[i] = dfs->handles[i + 1];
        dfs->entries[i] = dfs->entries[i + 1];
    }
}

u32 dfs_getDirListing(System *sys, const char *path, FileInfo **listing)
{
#ifdef UNIX
    DIR *dp;
    struct dirent *ep;
    struct stat st;
    char *fullpath;
#else
    WIN32_FIND_DATA ffd;
    HANDLE hfind = INVALID_HANDLE_VALUE;
#endif
    s32 nument = 0;

    char *realpath = dfs_getRealPath(sys, path);

    *listing = NULL;
#ifndef UNIX
    realpath = realloc(realpath, strlen(realpath) + 3);
    strcat(realpath, "\\*");
#endif

    /* Determinate the number of files in the directory */
#ifdef UNIX
    dp = opendir(realpath);
    if (dp != NULL)
#else
    hfind = FindFirstFile(realpath, &ffd);
    if (hfind != INVALID_HANDLE_VALUE)
#endif
    {
#ifdef UNIX
        while ((ep = readdir(dp)))
            if (ep->d_name[0] != '.')
#else
        nument = 1;
        do
            if (ffd.cFileName[0] != '.')
#endif
                nument++;
#ifdef UNIX
        closedir(dp);
#else
        while (FindNextFile(hfind, &ffd) != 0);
        FindClose(hfind);
#endif
    }
    else
    {
        _log(ERR, FS, "Can't read directory %s!", realpath);
        dfs_freeRealPath(realpath);
        return -1;
    }

    *listing = malloc(sizeof(FileInfo) * nument);

    /* Find the file infos */
#ifdef UNIX
    dp = opendir(realpath);
    if (dp != NULL)
#else
    hfind = FindFirstFile(realpath, &ffd);
    if (hfind != INVALID_HANDLE_VALUE)
#endif
    {
        s32 i = 0;
#ifdef UNIX
        while ((ep = readdir(dp)))
#else
        do
#endif
        {
#ifdef UNIX
            u8 fr, fw, fdw;
            if (ep->d_name[0] == '.')
                continue;
            fullpath = malloc(strlen(realpath) + strlen(ep->d_name) + 2);
            fullpath[0] = '\0';
            strcpy(fullpath, realpath);
            strcat(fullpath, "/");
            strcat(fullpath, ep->d_name);

            stat(fullpath, &st);

            /* name */
            strcpy((*listing)[i].name, ep->d_name);

            /* access */
            fr = (access(fullpath, R_OK) == 0);
            fw = (access(fullpath, W_OK) == 0);
            fdw = (access(realpath, W_OK) == 0);
            (*listing)[i].access = 0;
            if (fr)
                (*listing)[i].access |= FILEACCESS_READ;
            if (fw)
                (*listing)[i].access |= FILEACCESS_WRITE | FILEACCESS_APPEND;
            if (fdw)
                (*listing)[i].access |= FILEACCESS_CREATE;

            /* size */
            (*listing)[i].size = st.st_size;

            /* dir/file */
            (*listing)[i].type = S_ISDIR(st.st_mode) ? FILETYPE_DIRECTORY : FILETYPE_NORMAL;

            free(fullpath);
#else
            if (ffd.cFileName[0] == '.')
                continue;
            /* name */
            strcpy((*listing)[i].name, ffd.cFileName);

            /* access: everything is accessible by default on Windows */
            (*listing)[i].access = FILEACCESS_READ | FILEACCESS_WRITE | FILEACCESS_APPEND | FILEACCESS_CREATE;

            /* size */
            (*listing)[i].size = ((u64)ffd.nFileSizeHigh << 32) | ffd.nFileSizeLow;

            /* dir/file */
            (*listing)[i].type = (ffd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) ? FILETYPE_DIRECTORY : FILETYPE_NORMAL;
#endif
            i++;
#ifdef UNIX
        }
        closedir(dp);
#else
        } while (FindNextFile(hfind, &ffd) != 0);
        FindClose(hfind);
#endif
        dfs_freeRealPath(realpath);
        return i;
    }
    else
    {
        _log(ERR, FS, "Can't read directory entities a second time for %s? WTF?", realpath);
        dfs_freeRealPath(realpath);
        return -1;
    }
}

void dfs_freeDirListing(FileInfo *listing)
{
    free(listing);
}

